import {
  BufferGeometry,
  FileLoader,
  Float32BufferAttribute,
  Group,
  LineBasicMaterial,
  LineSegments,
  Loader,
  Material,
  Mesh,
  MeshPhongMaterial,
  Points,
  PointsMaterial,
  Vector3,
} from 'three'

// o object_name | g group_name
const _object_pattern = /^[og]\s*(.+)?/
// mtllib file_reference
const _material_library_pattern = /^mtllib /
// usemtl material_name
const _material_use_pattern = /^usemtl /
// usemap map_name
const _map_use_pattern = /^usemap /

const _vA = /* @__PURE__ */ new Vector3()
const _vB = /* @__PURE__ */ new Vector3()
const _vC = /* @__PURE__ */ new Vector3()

const _ab = /* @__PURE__ */ new Vector3()
const _cb = /* @__PURE__ */ new Vector3()

function ParserState() {
  const state = {
    objects: [],
    object: {},

    vertices: [],
    normals: [],
    colors: [],
    uvs: [],

    materials: {},
    materialLibraries: [],

    startObject: function (name, fromDeclaration) {
      // If the current object (initial from reset) is not from a g/o declaration in the parsed
      // file. We need to use it for the first parsed g/o to keep things in sync.
      if (this.object && this.object.fromDeclaration === false) {
        this.object.name = name
        this.object.fromDeclaration = fromDeclaration !== false
        return
      }

      const previousMaterial =
        this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined

      if (this.object && typeof this.object._finalize === 'function') {
        this.object._finalize(true)
      }

      this.object = {
        name: name || '',
        fromDeclaration: fromDeclaration !== false,

        geometry: {
          vertices: [],
          normals: [],
          colors: [],
          uvs: [],
          hasUVIndices: false,
        },
        materials: [],
        smooth: true,

        startMaterial: function (name, libraries) {
          const previous = this._finalize(false)

          // New usemtl declaration overwrites an inherited material, except if faces were declared
          // after the material, then it must be preserved for proper MultiMaterial continuation.
          if (previous && (previous.inherited || previous.groupCount <= 0)) {
            this.materials.splice(previous.index, 1)
          }

          const material = {
            index: this.materials.length,
            name: name || '',
            mtllib: Array.isArray(libraries) && libraries.length > 0 ? libraries[libraries.length - 1] : '',
            smooth: previous !== undefined ? previous.smooth : this.smooth,
            groupStart: previous !== undefined ? previous.groupEnd : 0,
            groupEnd: -1,
            groupCount: -1,
            inherited: false,

            clone: function (index) {
              const cloned = {
                index: typeof index === 'number' ? index : this.index,
                name: this.name,
                mtllib: this.mtllib,
                smooth: this.smooth,
                groupStart: 0,
                groupEnd: -1,
                groupCount: -1,
                inherited: false,
              }
              cloned.clone = this.clone.bind(cloned)
              return cloned
            },
          }

          this.materials.push(material)

          return material
        },

        currentMaterial: function () {
          if (this.materials.length > 0) {
            return this.materials[this.materials.length - 1]
          }

          return undefined
        },

        _finalize: function (end) {
          const lastMultiMaterial = this.currentMaterial()
          if (lastMultiMaterial && lastMultiMaterial.groupEnd === -1) {
            lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3
            lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart
            lastMultiMaterial.inherited = false
          }

          // Ignore objects tail materials if no face declarations followed them before a new o/g started.
          if (end && this.materials.length > 1) {
            for (let mi = this.materials.length - 1; mi >= 0; mi--) {
              if (this.materials[mi].groupCount <= 0) {
                this.materials.splice(mi, 1)
              }
            }
          }

          // Guarantee at least one empty material, this makes the creation later more straight forward.
          if (end && this.materials.length === 0) {
            this.materials.push({
              name: '',
              smooth: this.smooth,
            })
          }

          return lastMultiMaterial
        },
      }

      // Inherit previous objects material.
      // Spec tells us that a declared material must be set to all objects until a new material is declared.
      // If a usemtl declaration is encountered while this new object is being parsed, it will
      // overwrite the inherited material. Exception being that there was already face declarations
      // to the inherited material, then it will be preserved for proper MultiMaterial continuation.

      if (previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function') {
        const declared = previousMaterial.clone(0)
        declared.inherited = true
        this.object.materials.push(declared)
      }

      this.objects.push(this.object)
    },

    finalize: function () {
      if (this.object && typeof this.object._finalize === 'function') {
        this.object._finalize(true)
      }
    },

    parseVertexIndex: function (value, len) {
      const index = parseInt(value, 10)
      return (index >= 0 ? index - 1 : index + len / 3) * 3
    },

    parseNormalIndex: function (value, len) {
      const index = parseInt(value, 10)
      return (index >= 0 ? index - 1 : index + len / 3) * 3
    },

    parseUVIndex: function (value, len) {
      const index = parseInt(value, 10)
      return (index >= 0 ? index - 1 : index + len / 2) * 2
    },

    addVertex: function (a, b, c) {
      const src = this.vertices
      const dst = this.object.geometry.vertices

      dst.push(src[a + 0], src[a + 1], src[a + 2])
      dst.push(src[b + 0], src[b + 1], src[b + 2])
      dst.push(src[c + 0], src[c + 1], src[c + 2])
    },

    addVertexPoint: function (a) {
      const src = this.vertices
      const dst = this.object.geometry.vertices

      dst.push(src[a + 0], src[a + 1], src[a + 2])
    },

    addVertexLine: function (a) {
      const src = this.vertices
      const dst = this.object.geometry.vertices

      dst.push(src[a + 0], src[a + 1], src[a + 2])
    },

    addNormal: function (a, b, c) {
      const src = this.normals
      const dst = this.object.geometry.normals

      dst.push(src[a + 0], src[a + 1], src[a + 2])
      dst.push(src[b + 0], src[b + 1], src[b + 2])
      dst.push(src[c + 0], src[c + 1], src[c + 2])
    },

    addFaceNormal: function (a, b, c) {
      const src = this.vertices
      const dst = this.object.geometry.normals

      _vA.fromArray(src, a)
      _vB.fromArray(src, b)
      _vC.fromArray(src, c)

      _cb.subVectors(_vC, _vB)
      _ab.subVectors(_vA, _vB)
      _cb.cross(_ab)

      _cb.normalize()

      dst.push(_cb.x, _cb.y, _cb.z)
      dst.push(_cb.x, _cb.y, _cb.z)
      dst.push(_cb.x, _cb.y, _cb.z)
    },

    addColor: function (a, b, c) {
      const src = this.colors
      const dst = this.object.geometry.colors

      if (src[a] !== undefined) dst.push(src[a + 0], src[a + 1], src[a + 2])
      if (src[b] !== undefined) dst.push(src[b + 0], src[b + 1], src[b + 2])
      if (src[c] !== undefined) dst.push(src[c + 0], src[c + 1], src[c + 2])
    },

    addUV: function (a, b, c) {
      const src = this.uvs
      const dst = this.object.geometry.uvs

      dst.push(src[a + 0], src[a + 1])
      dst.push(src[b + 0], src[b + 1])
      dst.push(src[c + 0], src[c + 1])
    },

    addDefaultUV: function () {
      const dst = this.object.geometry.uvs

      dst.push(0, 0)
      dst.push(0, 0)
      dst.push(0, 0)
    },

    addUVLine: function (a) {
      const src = this.uvs
      const dst = this.object.geometry.uvs

      dst.push(src[a + 0], src[a + 1])
    },

    addFace: function (a, b, c, ua, ub, uc, na, nb, nc) {
      const vLen = this.vertices.length

      let ia = this.parseVertexIndex(a, vLen)
      let ib = this.parseVertexIndex(b, vLen)
      let ic = this.parseVertexIndex(c, vLen)

      this.addVertex(ia, ib, ic)
      this.addColor(ia, ib, ic)

      // normals

      if (na !== undefined && na !== '') {
        const nLen = this.normals.length

        ia = this.parseNormalIndex(na, nLen)
        ib = this.parseNormalIndex(nb, nLen)
        ic = this.parseNormalIndex(nc, nLen)

        this.addNormal(ia, ib, ic)
      } else {
        this.addFaceNormal(ia, ib, ic)
      }

      // uvs

      if (ua !== undefined && ua !== '') {
        const uvLen = this.uvs.length

        ia = this.parseUVIndex(ua, uvLen)
        ib = this.parseUVIndex(ub, uvLen)
        ic = this.parseUVIndex(uc, uvLen)

        this.addUV(ia, ib, ic)

        this.object.geometry.hasUVIndices = true
      } else {
        // add placeholder values (for inconsistent face definitions)

        this.addDefaultUV()
      }
    },

    addPointGeometry: function (vertices) {
      this.object.geometry.type = 'Points'

      const vLen = this.vertices.length

      for (let vi = 0, l = vertices.length; vi < l; vi++) {
        const index = this.parseVertexIndex(vertices[vi], vLen)

        this.addVertexPoint(index)
        this.addColor(index)
      }
    },

    addLineGeometry: function (vertices, uvs) {
      this.object.geometry.type = 'Line'

      const vLen = this.vertices.length
      const uvLen = this.uvs.length

      for (let vi = 0, l = vertices.length; vi < l; vi++) {
        this.addVertexLine(this.parseVertexIndex(vertices[vi], vLen))
      }

      for (let uvi = 0, l = uvs.length; uvi < l; uvi++) {
        this.addUVLine(this.parseUVIndex(uvs[uvi], uvLen))
      }
    },
  }

  state.startObject('', false)

  return state
}

//

class OBJLoader extends Loader {
  constructor(manager) {
    super(manager)

    this.materials = null
  }

  load(url, onLoad, onProgress, onError) {
    const scope = this

    const loader = new FileLoader(this.manager)
    loader.setPath(this.path)
    loader.setRequestHeader(this.requestHeader)
    loader.setWithCredentials(this.withCredentials)
    loader.load(
      url,
      function (text) {
        try {
          onLoad(scope.parse(text))
        } catch (e) {
          if (onError) {
            onError(e)
          } else {
            console.error(e)
          }

          scope.manager.itemError(url)
        }
      },
      onProgress,
      onError,
    )
  }

  setMaterials(materials) {
    this.materials = materials

    return this
  }

  parse(text) {
    const state = new ParserState()

    if (text.indexOf('\r\n') !== -1) {
      // This is faster than String.split with regex that splits on both
      text = text.replace(/\r\n/g, '\n')
    }

    if (text.indexOf('\\\n') !== -1) {
      // join lines separated by a line continuation character (\)
      text = text.replace(/\\\n/g, '')
    }

    const lines = text.split('\n')
    let line = '',
      lineFirstChar = ''
    let lineLength = 0
    let result = []

    // Faster to just trim left side of the line. Use if available.
    const trimLeft = typeof ''.trimLeft === 'function'

    for (let i = 0, l = lines.length; i < l; i++) {
      line = lines[i]

      line = trimLeft ? line.trimLeft() : line.trim()

      lineLength = line.length

      if (lineLength === 0) continue

      lineFirstChar = line.charAt(0)

      // @todo invoke passed in handler if any
      if (lineFirstChar === '#') continue

      if (lineFirstChar === 'v') {
        const data = line.split(/\s+/)

        switch (data[0]) {
          case 'v':
            state.vertices.push(parseFloat(data[1]), parseFloat(data[2]), parseFloat(data[3]))
            if (data.length >= 7) {
              state.colors.push(parseFloat(data[4]), parseFloat(data[5]), parseFloat(data[6]))
            } else {
              // if no colors are defined, add placeholders so color and vertex indices match

              state.colors.push(undefined, undefined, undefined)
            }

            break
          case 'vn':
            state.normals.push(parseFloat(data[1]), parseFloat(data[2]), parseFloat(data[3]))
            break
          case 'vt':
            state.uvs.push(parseFloat(data[1]), parseFloat(data[2]))
            break
        }
      } else if (lineFirstChar === 'f') {
        const lineData = line.substr(1).trim()
        const vertexData = lineData.split(/\s+/)
        const faceVertices = []

        // Parse the face vertex data into an easy to work with format

        for (let j = 0, jl = vertexData.length; j < jl; j++) {
          const vertex = vertexData[j]

          if (vertex.length > 0) {
            const vertexParts = vertex.split('/')
            faceVertices.push(vertexParts)
          }
        }

        // Draw an edge between the first vertex and all subsequent vertices to form an n-gon

        const v1 = faceVertices[0]

        for (let j = 1, jl = faceVertices.length - 1; j < jl; j++) {
          const v2 = faceVertices[j]
          const v3 = faceVertices[j + 1]

          state.addFace(v1[0], v2[0], v3[0], v1[1], v2[1], v3[1], v1[2], v2[2], v3[2])
        }
      } else if (lineFirstChar === 'l') {
        const lineParts = line.substring(1).trim().split(' ')
        let lineVertices = []
        const lineUVs = []

        if (line.indexOf('/') === -1) {
          lineVertices = lineParts
        } else {
          for (let li = 0, llen = lineParts.length; li < llen; li++) {
            const parts = lineParts[li].split('/')

            if (parts[0] !== '') lineVertices.push(parts[0])
            if (parts[1] !== '') lineUVs.push(parts[1])
          }
        }

        state.addLineGeometry(lineVertices, lineUVs)
      } else if (lineFirstChar === 'p') {
        const lineData = line.substr(1).trim()
        const pointData = lineData.split(' ')

        state.addPointGeometry(pointData)
      } else if ((result = _object_pattern.exec(line)) !== null) {
        // o object_name
        // or
        // g group_name

        // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869
        // let name = result[ 0 ].substr( 1 ).trim();
        const name = (' ' + result[0].substr(1).trim()).substr(1)

        state.startObject(name)
      } else if (_material_use_pattern.test(line)) {
        // material

        state.object.startMaterial(line.substring(7).trim(), state.materialLibraries)
      } else if (_material_library_pattern.test(line)) {
        // mtl file

        state.materialLibraries.push(line.substring(7).trim())
      } else if (_map_use_pattern.test(line)) {
        // the line is parsed but ignored since the loader assumes textures are defined MTL files
        // (according to https://www.okino.com/conv/imp_wave.htm, 'usemap' is the old-style Wavefront texture reference method)

        console.warn(
          'THREE.OBJLoader: Rendering identifier "usemap" not supported. Textures must be defined in MTL files.',
        )
      } else if (lineFirstChar === 's') {
        result = line.split(' ')

        // smooth shading

        // @todo Handle files that have varying smooth values for a set of faces inside one geometry,
        // but does not define a usemtl for each face set.
        // This should be detected and a dummy material created (later MultiMaterial and geometry groups).
        // This requires some care to not create extra material on each smooth value for "normal" obj files.
        // where explicit usemtl defines geometry groups.
        // Example asset: examples/models/obj/cerberus/Cerberus.obj

        /*
         * http://paulbourke.net/dataformats/obj/
         * or
         * http://www.cs.utah.edu/~boulos/cs3505/obj_spec.pdf
         *
         * From chapter "Grouping" Syntax explanation "s group_number":
         * "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off.
         * Polygonal elements use group numbers to put elements in different smoothing groups. For free-form
         * surfaces, smoothing groups are either turned on or off; there is no difference between values greater
         * than 0."
         */
        if (result.length > 1) {
          const value = result[1].trim().toLowerCase()
          state.object.smooth = value !== '0' && value !== 'off'
        } else {
          // ZBrush can produce "s" lines #11707
          state.object.smooth = true
        }

        const material = state.object.currentMaterial()
        if (material) material.smooth = state.object.smooth
      } else {
        // Handle null terminated files without exception
        if (line === '\0') continue

        console.warn('THREE.OBJLoader: Unexpected line: "' + line + '"')
      }
    }

    state.finalize()

    const container = new Group()
    container.materialLibraries = [].concat(state.materialLibraries)

    const hasPrimitives = !(state.objects.length === 1 && state.objects[0].geometry.vertices.length === 0)

    if (hasPrimitives === true) {
      for (let i = 0, l = state.objects.length; i < l; i++) {
        const object = state.objects[i]
        const geometry = object.geometry
        const materials = object.materials
        const isLine = geometry.type === 'Line'
        const isPoints = geometry.type === 'Points'
        let hasVertexColors = false

        // Skip o/g line declarations that did not follow with any faces
        if (geometry.vertices.length === 0) continue

        const buffergeometry = new BufferGeometry()

        buffergeometry.setAttribute('position', new Float32BufferAttribute(geometry.vertices, 3))

        if (geometry.normals.length > 0) {
          buffergeometry.setAttribute('normal', new Float32BufferAttribute(geometry.normals, 3))
        }

        if (geometry.colors.length > 0) {
          hasVertexColors = true
          buffergeometry.setAttribute('color', new Float32BufferAttribute(geometry.colors, 3))
        }

        if (geometry.hasUVIndices === true) {
          buffergeometry.setAttribute('uv', new Float32BufferAttribute(geometry.uvs, 2))
        }

        // Create materials

        const createdMaterials = []

        for (let mi = 0, miLen = materials.length; mi < miLen; mi++) {
          const sourceMaterial = materials[mi]
          const materialHash = sourceMaterial.name + '_' + sourceMaterial.smooth + '_' + hasVertexColors
          let material = state.materials[materialHash]

          if (this.materials !== null) {
            material = this.materials.create(sourceMaterial.name)

            // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material.
            if (isLine && material && !(material instanceof LineBasicMaterial)) {
              const materialLine = new LineBasicMaterial()
              Material.prototype.copy.call(materialLine, material)
              materialLine.color.copy(material.color)
              material = materialLine
            } else if (isPoints && material && !(material instanceof PointsMaterial)) {
              const materialPoints = new PointsMaterial({ size: 10, sizeAttenuation: false })
              Material.prototype.copy.call(materialPoints, material)
              materialPoints.color.copy(material.color)
              materialPoints.map = material.map
              material = materialPoints
            }
          }

          if (material === undefined) {
            if (isLine) {
              material = new LineBasicMaterial()
            } else if (isPoints) {
              material = new PointsMaterial({ size: 1, sizeAttenuation: false })
            } else {
              material = new MeshPhongMaterial()
            }

            material.name = sourceMaterial.name
            material.flatShading = sourceMaterial.smooth ? false : true
            material.vertexColors = hasVertexColors

            state.materials[materialHash] = material
          }

          createdMaterials.push(material)
        }

        // Create mesh

        let mesh

        if (createdMaterials.length > 1) {
          for (let mi = 0, miLen = materials.length; mi < miLen; mi++) {
            const sourceMaterial = materials[mi]
            buffergeometry.addGroup(sourceMaterial.groupStart, sourceMaterial.groupCount, mi)
          }

          if (isLine) {
            mesh = new LineSegments(buffergeometry, createdMaterials)
          } else if (isPoints) {
            mesh = new Points(buffergeometry, createdMaterials)
          } else {
            mesh = new Mesh(buffergeometry, createdMaterials)
          }
        } else {
          if (isLine) {
            mesh = new LineSegments(buffergeometry, createdMaterials[0])
          } else if (isPoints) {
            mesh = new Points(buffergeometry, createdMaterials[0])
          } else {
            mesh = new Mesh(buffergeometry, createdMaterials[0])
          }
        }

        mesh.name = object.name

        container.add(mesh)
      }
    } else {
      // if there is only the default parser state object with no geometry data, interpret data as point cloud

      if (state.vertices.length > 0) {
        const material = new PointsMaterial({ size: 1, sizeAttenuation: false })

        const buffergeometry = new BufferGeometry()

        buffergeometry.setAttribute('position', new Float32BufferAttribute(state.vertices, 3))

        if (state.colors.length > 0 && state.colors[0] !== undefined) {
          buffergeometry.setAttribute('color', new Float32BufferAttribute(state.colors, 3))
          material.vertexColors = true
        }

        const points = new Points(buffergeometry, material)
        container.add(points)
      }
    }

    return container
  }
}

export { OBJLoader }
